// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';

import 'package:web_engine_tester/golden_tester.dart';
import '../common/test_initialization.dart';
import 'paragraph/helper.dart';

DomElement get sceneHost =>
    EnginePlatformDispatcher.instance.implicitView!.dom.renderingHost
        .querySelector(DomManager.sceneHostTagName)!;

void main() {
  internalBootstrapBrowserTest(() => testMain);
}

Future<void> testMain() async {
  const Rect region = Rect.fromLTWH(0, 0, 500, 100);

  late BitmapCanvas canvas;

  void appendToScene() {
    // Create a <flt-scene> element to make sure our CSS reset applies correctly.
    final DomElement testScene = createDomElement('flt-scene');
    if (isIosSafari) {
      // Shrink to fit on the iPhone screen.
      testScene.style.position = 'absolute';
      testScene.style.transformOrigin = '0 0 0';
      testScene.style.transform = 'scale(0.3)';
    }
    testScene.append(canvas.rootElement);
    sceneHost.append(testScene);
  }

  setUpUnitTests(
    withImplicitView: true,
    emulateTesterEnvironment: false,
    setUpTestViewDimensions: false,
  );

  tearDown(() {
    sceneHost.querySelector('flt-scene')?.remove();
  });

  /// Draws several lines, some aligned precisely with the pixel grid, and some
  /// that are offset by 0.5 vertically or horizontally.
  ///
  /// The produced picture stresses the antialiasing generated by the browser
  /// when positioning and rasterizing `<canvas>` tags. Aliasing artifacts can
  /// be seen depending on pixel alignment and whether antialiasing happens
  /// before or after rasterization.
  void drawMisalignedLines(BitmapCanvas canvas) {
    final SurfacePaintData linePaint = (SurfacePaint()
          ..style = PaintingStyle.stroke
          ..strokeWidth = 1)
        .paintData;

    final SurfacePaintData fillPaint =
        (SurfacePaint()..style = PaintingStyle.fill).paintData;

    canvas.translate(10, 10);

    canvas.drawRect(
      const Rect.fromLTWH(0, 0, 40, 40),
      linePaint,
    );

    canvas.drawLine(
      const Offset(10, 0),
      const Offset(10, 40),
      linePaint,
    );

    canvas.drawLine(
      const Offset(20.5, 0),
      const Offset(20, 40),
      linePaint,
    );

    canvas.drawCircle(const Offset(30, 10), 3, fillPaint);
    canvas.drawCircle(const Offset(30.5, 30), 3, fillPaint);
  }

  test('renders pixels that are not aligned inside the canvas', () async {
    canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 60, 60),
        RenderStrategy());

    drawMisalignedLines(canvas);

    appendToScene();

    await matchGoldenFile('misaligned_pixels_in_canvas_test.png', region: region);
  });

  test('compensates for misalignment of the canvas', () async {
    // Notice the 0.5 offset in the bounds rectangle. It's what causes the
    // misalignment of canvas relative to the pixel grid. BitmapCanvas will
    // shift its position back to 0.0 and at the same time it will it will
    // compensate by shifting the contents of the canvas in the opposite
    // direction.
    canvas = BitmapCanvas(const Rect.fromLTWH(0.5, 0.5, 60, 60),
        RenderStrategy());
    canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
    drawMisalignedLines(canvas);

    appendToScene();

    await matchGoldenFile('misaligned_canvas_test.png', region: region);
  });

  test('fill the whole canvas with color even when transformed', () async {
    canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50),
        RenderStrategy());
    canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
    canvas.translate(25, 25);
    canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1.0), BlendMode.src);

    appendToScene();

    await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png',
        region: region);
  });

  test('fill the whole canvas with paint even when transformed', () async {
    canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50),
        RenderStrategy());
    canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
    canvas.translate(25, 25);
    canvas.drawPaint(SurfacePaintData()
      ..color = const Color.fromRGBO(0, 255, 0, 1.0).value
      ..style = PaintingStyle.fill);

    appendToScene();

    await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png',
        region: region);
  });

  // This test reproduces text blurriness when two pieces of text appear inside
  // two nested clips:
  //
  //   ┌───────────────────────┐
  //   │   text in outer clip  │
  //   │ ┌────────────────────┐│
  //   │ │ text in inner clip ││
  //   │ └────────────────────┘│
  //   └───────────────────────┘
  //
  // This test clips using canvas. See a similar test in `compositing_golden_test.dart`,
  // which clips using layers.
  //
  // More details: https://github.com/flutter/flutter/issues/32274
  test('renders clipped DOM text with high quality', () async {
    final CanvasParagraph paragraph =
        (ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))
          ..addText('Am I blurry?')).build() as CanvasParagraph;
    paragraph.layout(const ParagraphConstraints(width: 1000));

    final Rect canvasSize = Rect.fromLTRB(
      0,
      0,
      paragraph.maxIntrinsicWidth + 16,
      2 * paragraph.height + 32,
    );
    final Rect outerClip =
        Rect.fromLTRB(0.5, 0.5, canvasSize.right, canvasSize.bottom);
    final Rect innerClip = Rect.fromLTRB(0.5, canvasSize.bottom / 2 + 0.5,
        canvasSize.right, canvasSize.bottom);

    canvas = BitmapCanvas(canvasSize, RenderStrategy());
    canvas.debugChildOverdraw = true;
    canvas.clipRect(outerClip, ClipOp.intersect);
    canvas.drawParagraph(paragraph, const Offset(8.5, 8.5));
    canvas.clipRect(innerClip, ClipOp.intersect);
    canvas.drawParagraph(paragraph, Offset(8.5, 8.5 + innerClip.top));

    expect(
      canvas.rootElement.querySelectorAll('flt-paragraph').map<String>((DomElement e) => e.innerText).toList(),
      <String>['Am I blurry?', 'Am I blurry?'],
      reason: 'Expected to render text using HTML',
    );

    appendToScene();

    await matchGoldenFile(
      'bitmap_canvas_draws_high_quality_text.png',
      region: canvasSize,
    );
  }, testOn: 'chrome');

  // NOTE: Chrome in --headless mode does not reproduce the bug that this test
  //       attempts to reproduce. However, it's still good to have this test
  //       for potential future regressions related to paint order.
  test('draws text on top of canvas when transformed and clipped', () async {
    final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
      fontFamily: 'Ahem',
      fontSize: 18,
    ));

    const String text = 'This text is intentionally very long to make sure that it '
      'breaks into multiple lines.';
    builder.addText(text);

    final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
    paragraph.layout(const ParagraphConstraints(width: 100));

    final Rect canvasSize = Offset.zero & const Size(500, 500);

    canvas = BitmapCanvas(canvasSize, RenderStrategy());
    canvas.debugChildOverdraw = true;

    final SurfacePaintData pathPaint = SurfacePaintData()
      ..color = 0xFF7F7F7F
      ..style = PaintingStyle.fill;

    const double r = 200.0;
    const double l = 50.0;

    final Path path = (Path()
      ..moveTo(-l, -l)
      ..lineTo(0, -r)
      ..lineTo(l, -l)
      ..lineTo(r, 0)
      ..lineTo(l, l)
      ..lineTo(0, r)
      ..lineTo(-l, l)
      ..lineTo(-r, 0)
      ..close()).shift(const Offset(250, 250));

    final SurfacePaintData borderPaint = SurfacePaintData()
      ..color = black.value
      ..style = PaintingStyle.stroke;

    canvas.drawPath(path, pathPaint);
    canvas.drawParagraph(paragraph, const Offset(180, 50));
    canvas.drawRect(Rect.fromLTWH(180, 50, paragraph.width, paragraph.height), borderPaint);

    expect(
      canvas.rootElement.querySelectorAll('flt-paragraph').map<String?>((DomElement e) => e.text).toList(),
      <String>[text],
      reason: 'Expected to render text using HTML',
    );

    final SceneBuilder sb = SceneBuilder();
    sb.pushTransform(Matrix4.diagonal3Values(EngineFlutterDisplay.instance.browserDevicePixelRatio,
        EngineFlutterDisplay.instance.browserDevicePixelRatio, 1.0).toFloat64());
    sb.pushTransform(Matrix4.rotationZ(math.pi / 2).toFloat64());
    sb.pushOffset(0, -500);
    sb.pushClipRect(canvasSize);
    sb.pop();
    sb.pop();
    sb.pop();
    sb.pop();
    final SurfaceScene scene = sb.build() as SurfaceScene;
    final DomElement sceneElement = scene.webOnlyRootElement!;
    if (isIosSafari) {
      // Shrink to fit on the iPhone screen.
      sceneElement.style.position = 'absolute';
      sceneElement.style.transformOrigin = '0 0 0';
      sceneElement.style.transform = 'scale(0.3)';
    }

    sceneElement.querySelector('flt-clip')!.append(canvas.rootElement);
    sceneHost.append(sceneElement);

    await matchGoldenFile(
      'bitmap_canvas_draws_text_on_top_of_canvas.png',
      region: canvasSize,
    );
  });

  // Regression test for https://github.com/flutter/flutter/issues/96498. When
  // a picture is made of just text that can be rendered using plain HTML,
  // BitmapCanvas should not create any <canvas> elements as they are expensive.
  test('does not allocate bitmap canvas just for text', () async {
    canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50), RenderStrategy());

    final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'));
    builder.addText('Hello');
    final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
    paragraph.layout(const ParagraphConstraints(width: 1000));

    canvas.drawParagraph(paragraph, const Offset(8.5, 8.5));
    expect(
      canvas.rootElement.querySelectorAll('canvas'),
      isEmpty,
    );
    expect(
      canvas.rootElement.querySelectorAll('flt-paragraph').single.innerText,
      'Hello',
    );
  });
}
